import {
  atom,
  clearStack,
  computed,
  context,
  type Fn,
  isConnected,
  reatomLinkedList,
  sleep,
  withInit,
  wrap,
} from '@reatom/core'
import { expect, test, vi } from 'vitest'

// eslint-disable-next-line unused-imports/no-unused-imports
import { Bind, DEBUG, h, hf, type JSX, mount, stylesheet } from '.'

clearStack()

DEBUG.extend(withInit(() => false))

const parent = atom(() => {
  const main = (<main />) as HTMLElement
  window.document.body.appendChild(main)

  return main
}, 'parent')

test('static props & children', () =>
  context.start(async () => {
    const element = <div id="some-id">Hello, world!</div>

    mount(parent(), element)
    await wrap(sleep())

    expect(element.tagName).toBe('DIV')
    expect(element.id).toBe('some-id')
    expect(element.childNodes.length).toBe(1)
    expect(element.textContent).toBe('Hello, world!')
  }))

test('dynamic props', () =>
  context.start(async () => {
    const val = atom('val', 'val')
    const prp = atom('prp', 'prp')
    const atr = atom('atr', 'atr')

    const element = <div id={val} prop:prp={prp} attr:atr={atr} />

    mount(parent(), element)
    await wrap(sleep())

    expect(element.id).toBe('val')
    expect((element as any).prp).toBe('prp')
    expect(element.getAttribute('atr')).toBe('atr')

    val.set('val1')
    prp.set('prp1')
    atr.set('atr1')

    await wrap(sleep())
    expect(element.id).toBe('val1')
    expect((element as any).prp).toBe('prp1')
    expect(element.getAttribute('atr')).toBe('atr1')
  }))

test('getter props', () =>
  context.start(async () => {
    const val = atom('val', 'val')
    const getter = () => val() + ' ' + val()
    const element = <div id={getter} />

    mount(parent(), element)
    await wrap(sleep())
    expect(element.id).toBe(getter())

    val.set('val1')
    await wrap(sleep())
    expect(element.id).toBe(getter())
  }))

test('children updates', () =>
  context.start(async () => {
    const val = atom('foo', 'val')

    const route = atom('a', 'route')
    const a = window.document.createElement('div')
    const b = window.document.createElement('div')

    const element = (
      <div>
        Static one. {val}
        {computed(() => (route() === 'a' ? a : b))}
      </div>
    )

    mount(parent(), element)
    await wrap(sleep())

    expect(element.childNodes.length).toBe(7)
    expect(element.childNodes[2]?.textContent).toBe('foo')
    expect(element.childNodes[5]).toBe(a)

    val.set('bar')
    await wrap(sleep())
    expect(element.childNodes[2]?.textContent).toBe('bar')

    expect(element.childNodes[5]).toBe(a)
    route.set('b')
    await wrap(sleep())
    expect(element.childNodes[5]).toBe(b)
  }))

test('dynamic children', () =>
  context.start(async () => {
    const children = atom(<div />)

    const element = <div>{children}</div>

    mount(parent(), element)
    await wrap(sleep())

    expect(element.childNodes.length).toBe(3)

    children.set(<div>Hello, world!</div>)
    await wrap(sleep())
    expect(element.childNodes[1]?.textContent).toBe('Hello, world!')

    const inner = <span>inner</span>
    children.set(<div>{inner}</div>)
    await wrap(sleep())
    expect(element.childNodes[1]?.childNodes[0]).toBe(inner)

    const before = atom('before', 'before')
    const after = atom('after', 'after')
    children.set(
      <div>
        {before}
        {inner}
        {after}
      </div>,
    )
    before.set('before...')
    await wrap(sleep())
    expect((element as HTMLDivElement).innerText).toBe('before...innerafter')
  }))

test('spreads', () =>
  context.start(async () => {
    const clickTrack = vi.fn()
    const props = atom({
      id: '1',
      'attr:b': '2',
      'on:click': clickTrack as Fn,
      $spread: {
        class: () => ['aaa', atom('bbb')],
        $spread: {
          'style:color': 'red',
        },
      },
    })

    const element = (<div $spread={props} />) as HTMLDivElement

    mount(parent(), element)
    await wrap(sleep())

    expect(element.id).toBe('1')
    expect(element.getAttribute('b')).toBe('2')
    expect(element.getAttribute('class')).toBe('aaa bbb')
    expect(element.getAttribute('style')).toBe('color: red;')
    expect(clickTrack.mock.calls.length).toBe(0)
    element.click()
    expect(clickTrack.mock.calls.length).toBe(1)
  }))

test.skip('spreads difference', () =>
  context.start(async () => {
    const props = atom<Partial<Record<'class' | 'id', string>>>({
      class: 'class',
    })
    const element = (<div $spread={props} />) as HTMLDivElement

    mount(parent(), element)
    await wrap(sleep())
    expect(element.className).toBe('class')
    expect(element.id).toBe('')

    props.set({ id: 'id' })
    await wrap(sleep())
    expect(element.className).toBe('')
    expect(element.id).toBe('id')
  }))

test('multiple render shared element', () =>
  context.start(async () => {
    const valueAtom = atom('abc', 'value')
    const element = <p>{valueAtom}</p>

    const Component = () => (
      <>
        <div id="1">{element}</div>
        <div id="2">{element}</div>
      </>
    )

    const childAtom = atom<JSX.Element | undefined>(
      <Component></Component>,
      'child',
    )
    const app = <div>{childAtom}</div>

    mount(parent(), app)
    await wrap(sleep())
    expect(app.innerHTML).toBe(
      '<!--child--><!----><div id="1"></div><div id="2"><p><!--value-->abc<!--value--></p></div><!----><!--child-->',
    )

    valueAtom.set('def')
    await wrap(sleep())
    expect(app.innerHTML).toBe(
      '<!--child--><!----><div id="1"></div><div id="2"><p><!--value-->def<!--value--></p></div><!----><!--child-->',
    )

    childAtom.set(undefined)
    await wrap(sleep())
    expect(app.innerHTML).toBe('<!--child--><!--child-->')

    childAtom.set(<Component></Component>)
    valueAtom.set('ghi')
    await wrap(sleep())
    expect(app.innerHTML).toBe(
      '<!--child--><!----><div id="1"></div><div id="2"><p><!--value-->ghi<!--value--></p></div><!----><!--child-->',
    )
  }))

test('fragment as child', () =>
  context.start(async () => {
    const child = (
      <>
        <div>foo</div>
        <>
          <div>bar</div>
        </>
      </>
    )
    mount(parent(), child)
    await wrap(sleep())

    expect(parent().childNodes.length).toBe(6)
    expect(parent().textContent).toBe('foobar')
  }))

test('array children', () =>
  context.start(async () => {
    const n = atom(1)
    const list = computed(() =>
      Array.from({ length: n() }, (_, i) => <li>{i + 1}</li>),
    )

    const element = (
      <ul>
        {list}
        <br />
      </ul>
    )

    mount(parent(), element)
    await wrap(sleep())
    expect(element.childNodes.length).toBe(4)
    expect(element.textContent).toBe('1')

    n.set(2)
    await wrap(sleep())
    expect(element.childNodes.length).toBe(5)
    expect(element.textContent).toBe('12')
  }))

test('linked list', () =>
  context.start(async () => {
    const list = reatomLinkedList((value: any) => atom(value))
    const jsxList = list.reatomMap((n) => <span>{n}</span>)
    const one = list.create(1)
    const two = list.create(2)

    mount(parent(), <div>{jsxList}</div>)

    await wrap(sleep())
    expect(parent().innerText).toBe('12')
    expect(isConnected(list)).toBe(true)
    expect(isConnected(jsxList)).toBe(true)
    expect(isConnected(one)).toBe(true)
    expect(isConnected(two)).toBe(true)
    expect(list.array()).toEqual([one, two])

    list.swap(one, two)
    await wrap(sleep())
    expect(list.array()).toEqual([two, one])
    expect(parent().innerText).toBe('21')

    list.remove(two)
    await wrap(sleep())
    expect(parent().innerText).toBe('1')
    expect(isConnected(one)).toBe(true)
    expect(isConnected(two)).toBe(false)

    list.create(<>3</>)
  }))

test('boolean as child', () =>
  context.start(async () => {
    const trueAtom = atom(true, 'true')
    const trueValue = true
    const falseAtom = atom(false, 'false')
    const falseValue = false

    const element = (
      <div>
        {trueAtom}
        {trueValue}
        {falseAtom}
        {falseValue}
      </div>
    )

    await wrap(sleep())
    expect(element.childNodes.length).toBe(4)
    expect(element.innerHTML).toBe(
      '<!--true--><!--true--><!--false--><!--false-->',
    )
    expect(element.textContent).toBe('')
  }))

test('null as child', () =>
  context.start(async () => {
    const nullAtom = atom(null, 'null')
    const nullValue = null

    const element = (
      <div>
        {nullAtom}
        {nullValue}
      </div>
    )

    await wrap(sleep())
    expect(element.childNodes.length).toBe(2)
    expect(element.innerHTML).toBe('<!--null--><!--null-->')
    expect(element.textContent).toBe('')
  }))

test('undefined as child', () =>
  context.start(async () => {
    const undefinedAtom = atom(undefined, 'undefined')
    const undefinedValue = undefined

    const element = (
      <div>
        {undefinedAtom}
        {undefinedValue}
      </div>
    )

    await wrap(sleep())
    expect(element.childNodes.length).toBe(2)
    expect(element.innerHTML).toBe('<!--undefined--><!--undefined-->')
    expect(element.textContent).toBe('')
  }))

test('empty string as child', () =>
  context.start(async () => {
    const emptyStringAtom = atom('', 'emptyString')
    const emptyStringValue = ''

    const element = (
      <div>
        {emptyStringAtom}
        {emptyStringValue}
      </div>
    )

    await wrap(sleep())
    expect(element.childNodes.length).toBe(2)
    expect(element.innerHTML).toBe('<!--emptyString--><!--emptyString-->')
    expect(element.textContent).toBe('')
  }))

test('update skipped atom', () =>
  context.start(async () => {
    const valueAtom = atom<number | undefined>(undefined, 'value')

    const element = <div>{valueAtom}</div>

    mount(parent(), element)
    await wrap(sleep())

    expect(parent().childNodes.length).toBe(1)
    expect(parent().textContent).toBe('')

    valueAtom.set(123)

    await wrap(sleep())
    expect(parent().childNodes.length).toBe(1)
    expect(parent().textContent).toBe('123')
  }))

test('render HTMLElement atom', () =>
  context.start(async () => {
    const htmlAtom = atom(<div>div</div>, 'html')
    const element = <div>{htmlAtom}</div>

    mount(parent(), element)
    await wrap(sleep())
    expect(element.innerHTML).toBe('<!--html--><div>div</div><!--html-->')
  }))

test('render SVGElement atom', () =>
  context.start(async () => {
    const svgAtom = atom(<svg:svg>svg</svg:svg>, 'svg')
    const element = <div>{svgAtom}</div>

    mount(parent(), element)
    await wrap(sleep())
    expect(element.innerHTML).toBe('<!--svg--><svg>svg</svg><!--svg-->')
  }))

test('custom component', () =>
  context.start(async () => {
    const Component = (props: JSX.HTMLAttributes) => <div {...props} />

    await wrap(sleep())
    expect(<Component />).toBeInstanceOf(window.HTMLElement)
    expect(((<Component draggable />) as HTMLElement).draggable).toBe(true)
    expect(((<Component>123</Component>) as HTMLElement).innerText).toBe('123')
  }))

test('ref unmount callback', () =>
  context.start(async () => {
    const Component = (props: JSX.HTMLAttributes) => <div {...props} />

    let ref: null | HTMLElement = null

    const component = (
      <Component
        ref={(el) => {
          ref = el
          return () => {
            ref = null
          }
        }}
      />
    )

    mount(parent(), component)
    await wrap(sleep())
    expect(ref).toBeInstanceOf(window.HTMLElement)

    parent().remove()
    await wrap(sleep())
    expect(ref).toBe(null)
  }))

test('child ref unmount callback', () =>
  context.start(async () => {
    const Component = (props: JSX.HTMLAttributes) => <div {...props} />

    let ref: null | HTMLElement = null

    const component = (
      <Component
        ref={(el) => {
          ref = el
          return () => {
            ref = null
          }
        }}
      />
    )

    mount(parent(), component)
    await wrap(sleep())
    expect(ref).toBeInstanceOf(window.HTMLElement)

    ref!.remove()
    await wrap(sleep())
    expect(ref).toBe(null)
  }))

test('same arguments in ref mount and unmount hooks', () =>
  context.start(async () => {
    let mountElement: HTMLElement
    let unmountElement: HTMLElement

    let ref: null | HTMLElement = null

    const component = (
      <div
        ref={(el) => {
          mountElement = el
          ref = el
          return (el) => {
            unmountElement = el
            ref = null
          }
        }}
      />
    )

    mount(parent(), component)
    await wrap(sleep())
    expect(ref).toBeInstanceOf(window.HTMLElement)

    ref!.remove()
    await wrap(sleep())
    expect(ref).toBe(null)
    expect(mountElement!).toBe(component)
    expect(unmountElement!).toBe(component)
  }))

test('css property and class attribute', () =>
  context.start(async () => {
    const cls = 'class'
    const css = 'color: red;'

    const ref1 = <div css={css} class={cls}></div>
    const ref2 = <div class={cls} css={css}></div>

    const component = (
      <div>
        {ref1}
        {ref2}
      </div>
    )

    mount(parent(), component)
    expect(ref1).toBeInstanceOf(window.HTMLElement)
    expect(ref2).toBeInstanceOf(window.HTMLElement)
    await wrap(sleep())

    expect(ref1.className).toBe(cls)
    expect(ref1.dataset['reatomStyle']).toBeTruthy()

    expect(ref2.className).toBe(cls)
    expect(ref2.dataset['reatomStyle']).toBeTruthy()

    expect(ref1.dataset['reatomStyle']).toBe(ref2.dataset['reatomStyle'])
  }))

test('css property generate class name', () =>
  context.start(async () => {
    const First = () => <div css="color: red;"></div> // same
    const Second = () => <div css="color: red;"></div> // same
    const Third = () => <div css="color: blue;"></div>

    DEBUG.set(true)

    const first = <First></First>
    const second = <Second></Second>
    const third = <Third></Third>

    const component = (
      <div>
        {first}
        {second}
        {third}
      </div>
    )

    mount(parent(), component)
    await wrap(sleep())

    expect({ ...first.dataset }).toEqual({
      reatomName: 'First',
      reatomStyle: '1', // same
    })
    expect({ ...second.dataset }).toEqual({
      reatomName: 'Second',
      reatomStyle: '1', // same
    })
    expect({ ...third.dataset }).toEqual({
      reatomName: 'Third',
      reatomStyle: '2',
    })
  }))

test('css custom property', () =>
  context.start(async () => {
    const colorAtom = atom('red' as string | undefined)

    const component = (
      <div css:first-property={colorAtom} css:secondProperty={colorAtom}></div>
    )

    mount(parent(), component)
    await wrap(sleep())

    expect(component.style.getPropertyValue('--first-property')).toBe('red')
    expect(component.style.getPropertyValue('--secondProperty')).toBe('red')

    colorAtom.set('green')

    await wrap(sleep())
    expect(component.style.getPropertyValue('--first-property')).toBe('green')
    expect(component.style.getPropertyValue('--secondProperty')).toBe('green')

    colorAtom.set(undefined)

    await wrap(sleep())
    expect(component.style.getPropertyValue('--first-property')).toBe('')
    expect(component.style.getPropertyValue('--secondProperty')).toBe('')
  }))

test('class and className attribute', () =>
  context.start(async () => {
    const classAtom = atom('' as string | undefined)

    const ref1 = <div class={classAtom}></div>
    const ref2 = <div className={classAtom}></div>

    const component = (
      <div>
        {ref1}
        {ref2}
      </div>
    )

    mount(parent(), component)
    await wrap(sleep())

    expect(ref1.hasAttribute('class')).toBe(true)
    expect(ref2.hasAttribute('class')).toBe(true)

    classAtom.set('cls')
    await wrap(sleep())
    expect(ref1.className).toBe('cls')
    expect(ref2.className).toBe('cls')
    expect(ref1.hasAttribute('class')).toBe(true)
    expect(ref2.hasAttribute('class')).toBe(true)

    classAtom.set(undefined)
    await wrap(sleep())
    expect(ref1.className).toBe('')
    expect(ref2.className).toBe('')
    expect(ref1.hasAttribute('class')).toBe(true)
    expect(ref2.hasAttribute('class')).toBe(true)
  }))

test('class handles complex correctly', () =>
  context.start(async () => {
    const isBAtom = atom(true)
    const stringAtom = atom('d')
    const element = (
      <div
        class={() => ['a', { b: isBAtom }, ['c'], stringAtom, () => 'e']}
      ></div>
    )

    mount(parent(), element)
    await wrap(sleep())
    expect(element.className).toBe('a b c d e')

    isBAtom.set(false)
    stringAtom.set('dd')
    await wrap(sleep())
    expect(element.className).toBe('a c dd e')
  }))

test('ref mount and unmount callbacks order', () =>
  context.start(async () => {
    const order: number[] = []

    const createRef = (index: number) => {
      return () => {
        order.push(index)
        return () => {
          order.push(index)
        }
      }
    }

    const component = (
      <div ref={createRef(0)}>
        <div ref={createRef(1)}>
          <div ref={createRef(2)}></div>
        </div>
      </div>
    )

    mount(parent(), component)
    await wrap(sleep())
    parent().remove()
    await wrap(sleep())

    expect(order).toStrictEqual([2, 1, 0, 0, 1, 2])
  }))

test('style object update', () =>
  context.start(async () => {
    const styleTopAtom = atom<JSX.StyleProperties['top']>('0')
    const styleRightAtom = atom<JSX.StyleProperties['right']>(undefined)
    const styleBottomAtom = atom<JSX.StyleProperties['bottom']>(null)
    const styleLeftAtom = atom<JSX.StyleProperties['left']>('0')
    const styleAtom = computed<JSX.StyleProperties>(() => ({
      top: styleTopAtom(),
      right: styleRightAtom(),
      bottom: styleBottomAtom(),
      left: styleLeftAtom(),
    }))

    const firstEl = <div style={styleAtom}></div>
    const secondEl = (
      <div
        style:top={styleTopAtom}
        style:right={styleRightAtom}
        style:bottom={styleBottomAtom}
        style:left={styleLeftAtom}
      ></div>
    )

    const component = (
      <div>
        {firstEl}
        {secondEl}
      </div>
    )

    mount(parent(), component)

    await wrap(sleep())
    expect(firstEl.getAttribute('style')).toBe('top: 0px; left: 0px;')
    expect(secondEl.getAttribute('style')).toBe('top: 0px; left: 0px;')

    styleTopAtom.set(undefined)
    styleBottomAtom.set(0)

    await wrap(sleep())
    expect(firstEl.getAttribute('style')).toBe('left: 0px; bottom: 0px;')
    expect(secondEl.getAttribute('style')).toBe('left: 0px; bottom: 0px;')
  }))

test('render atom fragments', () =>
  context.start(async () => {
    const bool1Atom = atom(false)
    const bool2Atom = atom(false)

    const element = (
      <div>
        <p>0</p>
        {computed(
          () =>
            bool1Atom() ? (
              <>
                <p>1</p>
                {computed(
                  () =>
                    bool2Atom() ? (
                      <>
                        <p>2</p>
                        <p>3</p>
                      </>
                    ) : undefined,
                  '2',
                )}
                <p>4</p>
              </>
            ) : undefined,
          '1',
        )}
        <p>5</p>
      </div>
    )

    mount(parent(), element)

    await wrap(sleep())

    const expect1 = '<p>0</p><!--1--><!--1--><p>5</p>'
    const expect2 =
      '<p>0</p><!--1--><!----><p>1</p><!--2--><!--2--><p>4</p><!----><!--1--><p>5</p>'
    const expect3 =
      '<p>0</p><!--1--><!----><p>1</p><!--2--><!----><p>2</p><p>3</p><!----><!--2--><p>4</p><!----><!--1--><p>5</p>'

    bool1Atom.set(false)
    bool2Atom.set(false)
    await wrap(sleep())
    expect(element.innerHTML).toBe(expect1)

    bool1Atom.set(false)
    bool2Atom.set(true)
    await wrap(sleep())
    expect(element.innerHTML).toBe(expect1)

    bool1Atom.set(true)
    bool2Atom.set(false)
    await wrap(sleep())
    expect(element.innerHTML).toBe(expect2)

    bool1Atom.set(true)
    bool2Atom.set(true)
    await wrap(sleep())
    expect(element.innerHTML).toBe(expect3)

    bool1Atom.set(true)
    bool2Atom.set(false)
    await wrap(sleep())
    expect(element.innerHTML).toBe(expect2)

    bool1Atom.set(true)
    bool2Atom.set(true)
    await wrap(sleep())
    expect(element.innerHTML).toBe(expect3)

    bool1Atom.set(false)
    bool2Atom.set(true)
    await wrap(sleep())
    expect(element.innerHTML).toBe(expect1)

    bool1Atom.set(false)
    bool2Atom.set(false)
    await wrap(sleep())
    expect(element.innerHTML).toBe(expect1)
  }))

test('Bind', () =>
  context.start(async () => {
    const div = (<div />) as HTMLDivElement
    const input = (<input />) as HTMLInputElement
    const svg = (<svg:svg />) as SVGSVGElement

    const inputState = atom('42')

    const testDiv = (
      <Bind
        element={div}
        // @ts-expect-error there should be an error here
        value={inputState}
      />
    )
    const testInput = (
      <Bind
        element={input}
        value={inputState}
        on:input={(e) => inputState.set(e.currentTarget.value)}
      />
    )
    const testSvg = (
      <Bind element={svg}>
        <svg:path d="M 10 10 H 100" />
      </Bind>
    )

    mount(
      parent(),
      <main>
        {testDiv}
        {testInput}
        {testSvg}
      </main>,
    )

    await wrap(sleep())

    inputState.set('43')

    await wrap(sleep())
    expect(input.value).toBe('43')
    expect(testSvg.innerHTML).toBe('<path d="M 10 10 H 100"></path>')
  }))

test('dynamic atom fragment', () =>
  context.start(async () => {
    const child = atom<JSX.HTMLAttributes['children']>(<span />, 'test')

    const container = <div>{child}</div>
    mount(parent(), container)

    await wrap(sleep())
    expect(container.outerHTML).toBe(
      '<div><!--test--><span></span><!--test--></div>',
    )

    child.set(() => atom('child atom', 'test.child'))
    await wrap(sleep())
    expect(container.outerHTML).toBe(
      '<div><!--test--><!--test.child-->child atom<!--test.child--><!--test--></div>',
    )
  }))

test('linked list', () =>
  context.start(async () => {
    const a = atom(true)
    const list = reatomLinkedList((value: string) => (
      <>
        <span>{value}</span>
        {computed(() => (a() ? <a /> : <br />), 'test')}
      </>
    ))
    list.create('1')

    const container = <div>{list}</div>
    mount(parent(), container)

    await wrap(sleep())
    expect(container.outerHTML).toBe(
      '<div><!----><span>1</span><!--test--><a></a><!--test--><!----></div>',
    )

    a.set(false)
    await wrap(sleep())
    expect(container.outerHTML).toBe(
      '<div><!----><span>1</span><!--test--><br><!--test--><!----></div>',
    )

    const node = list.create('2')
    await wrap(sleep())
    expect(container.outerHTML).toBe(
      '<div><!----><span>1</span><!--test--><br><!--test--><!----><!----><span>2</span><!--test--><br><!--test--><!----></div>',
    )

    list.remove(node)
    await wrap(sleep())
    expect(container.outerHTML).toBe(
      '<div><!----><span>1</span><!--test--><br><!--test--><!----></div>',
    )
  }))

test('linked list createMany', () =>
  context.start(async () => {
    const list = reatomLinkedList((value: number) => <span>{value}</span>)

    const container = <div>{list}</div>
    mount(parent(), container)

    await wrap(sleep())
    expect(container.innerHTML).toBe('')

    const nodes = list.createMany([[1], [2], [3]])
    await wrap(sleep())
    expect(container.innerHTML).toBe('<span>1</span><span>2</span><span>3</span>')
    expect(list.array()).toEqual(nodes)

    list.createMany([[4], [5]])
    await wrap(sleep())
    expect(container.innerHTML).toBe(
      '<span>1</span><span>2</span><span>3</span><span>4</span><span>5</span>',
    )
  }))

test('linked list removeMany', () =>
  context.start(async () => {
    const list = reatomLinkedList((value: number) => <span>{value}</span>)
    const [one, two, three, four] = list.createMany([[1], [2], [3], [4]])

    const container = <div>{list}</div>
    mount(parent(), container)

    await wrap(sleep())
    expect(container.innerHTML).toBe(
      '<span>1</span><span>2</span><span>3</span><span>4</span>',
    )

    list.removeMany([two!, four!])
    await wrap(sleep())
    expect(container.innerHTML).toBe('<span>1</span><span>3</span>')

    list.removeMany([one!, three!])
    await wrap(sleep())
    expect(container.innerHTML).toBe('')
  }))

test('linked list createMany and removeMany with reatomMap', () =>
  context.start(async () => {
    const list = reatomLinkedList((value: number) => atom(value))
    const jsxList = list.reatomMap((n) => <span>{n}</span>)

    const container = <div>{jsxList}</div>
    mount(parent(), container)

    await wrap(sleep())
    expect(container.innerHTML).toBe('')
    expect(isConnected(list)).toBe(true)
    expect(isConnected(jsxList)).toBe(true)

    const nodes = list.createMany([[1], [2], [3]])
    await wrap(sleep())
    expect(parent().innerText).toBe('123')
    expect(list.array()).toEqual(nodes)

    list.removeMany([nodes[0]!, nodes[2]!])
    await wrap(sleep())
    expect(parent().innerText).toBe('2')
    expect(list.array()).toEqual([nodes[1]])
  }))

const expectHtmlElementProperty = <
  Tag extends keyof JSX.HTMLElementTags,
  Property extends keyof JSX.HTMLElementTags[Tag],
  Value extends JSX.HTMLElementTags[Tag][Property],
  Expected extends Tag extends keyof HTMLElementTagNameMap
    ? Property extends keyof HTMLElementTagNameMap[Tag]
      ? HTMLElementTagNameMap[Tag][Property]
      : never
    : never,
>(
  tag: Tag,
  prop: Property & string,
  value: Value,
  expected: Expected,
  hasAttr: boolean,
  getAttr: null | string,
) => {
  const element = h(tag, { [prop]: value }) as HTMLElement
  expect((element as any)[prop]).toBe(expected)
  expect(element.hasAttribute(prop.toLowerCase())).toBe(hasAttr)
  expect(element.getAttribute(prop.toLowerCase())).toBe(getAttr)
}

test('width property', () =>
  context.start(async () => {
    expectHtmlElementProperty('img', 'width', undefined, 0, false, null)
    expectHtmlElementProperty('img', 'width', null, 0, false, null)
    expectHtmlElementProperty('img', 'width', 1, 1, true, '1')
    expectHtmlElementProperty('img', 'width', '1', 1, true, '1')
    expectHtmlElementProperty('img', 'width', -1, 0, true, '-1')
  }))

test('height property', () =>
  context.start(async () => {
    expectHtmlElementProperty('img', 'height', undefined, 0, false, null)
    expectHtmlElementProperty('img', 'height', null, 0, false, null)
    expectHtmlElementProperty('img', 'height', 1, 1, true, '1')
    expectHtmlElementProperty('img', 'height', '1', 1, true, '1')
    expectHtmlElementProperty('img', 'height', -1, 0, true, '-1')
  }))

test('download property', () =>
  context.start(async () => {
    expectHtmlElementProperty('a', 'download', undefined, '', false, null)
    expectHtmlElementProperty('a', 'download', null, '', false, null)
    expectHtmlElementProperty('a', 'download', 'abc', 'abc', true, 'abc')
  }))

test('href property', () =>
  context.start(async () => {
    expectHtmlElementProperty('a', 'href', undefined, '', false, null)
    expectHtmlElementProperty('a', 'href', null, '', false, null)
    expectHtmlElementProperty(
      'a',
      'href',
      'https://test.com/',
      'https://test.com/',
      true,
      'https://test.com/',
    )
  }))

test('role property', () =>
  context.start(async () => {
    expectHtmlElementProperty('div', 'role', undefined, null, false, null)
    expectHtmlElementProperty('div', 'role', null, null, false, null)
    expectHtmlElementProperty('div', 'role', 'alert', 'alert', true, 'alert')
  }))

test('list property', () =>
  context.start(async () => {
    const element = (<input list="list"></input>) as HTMLInputElement
    const list = (<datalist id="list"></datalist>) as HTMLDataListElement
    mount(
      parent(),
      <div>
        {element}
        {list}
      </div>,
    )

    expect(element.list).toBe(list)
    expect(element.hasAttribute('list')).toBe(true)
    expect(element.getAttribute('list')).toBe('list')

    expectHtmlElementProperty('input', 'list', undefined, null, false, null)
    expectHtmlElementProperty('input', 'list', null, null, false, null)
  }))

test('form property', () =>
  context.start(async () => {
    const element = (<input form="form"></input>) as HTMLInputElement
    const form = (<form id="form"></form>) as HTMLFormElement
    mount(
      parent(),
      <div>
        {element}
        {form}
      </div>,
    )

    expect(element.form).toBe(form)
    expect(element.hasAttribute('form')).toBe(true)
    expect(element.getAttribute('form')).toBe('form')

    expectHtmlElementProperty('input', 'form', undefined, null, false, null)
    expectHtmlElementProperty('input', 'form', null, null, false, null)
  }))

// test('tabIndex property', () =>
//   context.start(async () => {
//     expectHtmlElementProperty('div', 'tabIndex', undefined, -1, false, null)
//     expectHtmlElementProperty('div', 'tabIndex', null, -1, false, null)
//     expectHtmlElementProperty('div', 'tabIndex', 0, 0, true, '0')
//     expectHtmlElementProperty('div', 'tabIndex', '0', 0, true, '0')
//   }))
// test('rowSpan property', () =>
//   context.start(async () => {
//     expectHtmlElementProperty('td', 'rowSpan', undefined, 1, false, null)
//     expectHtmlElementProperty('td', 'rowSpan', null, 1, false, null)
//     expectHtmlElementProperty('td', 'rowSpan', 0, 0, true, '0')
//     expectHtmlElementProperty('td', 'rowSpan', -1, 1, true, '-1')
//   }))
// test('colSpan property', () =>
//   context.start(async () => {
//     expectHtmlElementProperty('td', 'colSpan', undefined, 1, false, null)
//     expectHtmlElementProperty('td', 'colSpan', null, 1, false, null)
//     expectHtmlElementProperty('td', 'colSpan', 1, 1, true, '1')
//     expectHtmlElementProperty('td', 'colSpan', 0, 1, true, '0')
//   }))

test('aria attributes', () =>
  context.start(async () => {
    const expectAttribute = <
      Attr extends keyof JSX.AriaAttributes,
      Value extends JSX.AriaAttributes[Attr],
    >(
      attr: Attr,
      value: Value,
    ) => {
      const element = h('div', { [attr]: value })
      expect(element.hasAttribute(attr)).toBe(value != null)
      expect(element.getAttribute(attr)).toBe(value?.toString() ?? null)
    }

    expectAttribute('aria-checked', undefined)
    expectAttribute('aria-checked', null)
    expectAttribute('aria-checked', false)
    expectAttribute('aria-checked', true)
    expectAttribute('aria-checked', 'false')
    expectAttribute('aria-checked', 'true')
    expectAttribute('aria-colcount', 1)
    expectAttribute('aria-colcount', '1')
  }))

test('custom stylesheet for css property', () =>
  context.start(async () => {
    const sheet = new CSSStyleSheet()
    document.adoptedStyleSheets.push(sheet)
    stylesheet.set(sheet)

    const element = <div css="display:flex"></div>

    mount(parent(), element)
    await wrap(sleep())

    expect(stylesheet()).toEqual(sheet)
    expect(stylesheet().cssRules.length).toBe(1)
    expect(element.computedStyleMap().get('display')!.toString(), 'flex')
  }))

test('element subscribes to atom when mounted to DOM', () =>
  context.start(async () => {
    const valueAtom = atom('aaa')
    const element = <div class={valueAtom}></div>

    mount(parent(), element)
    await wrap(sleep())

    expect(isConnected(valueAtom)).toBe(true)
    expect(element.className).toBe('aaa')

    valueAtom.set('bbb')
    await wrap(sleep())
    parent().append(element)
    await wrap(sleep())

    expect(isConnected(valueAtom)).toBe(true)
    expect(element.className).toBe('bbb')
  }))

test('element unsubscribes from atom when removed from DOM', () =>
  context.start(async () => {
    const valueAtom = atom('aaa')
    const element = <div class={valueAtom}></div>

    await wrap(sleep())
    expect(isConnected(valueAtom)).toBe(false)
    expect(element.className).toBe('')

    mount(parent(), element)
    await wrap(sleep())
    element.remove()
    await wrap(sleep())

    expect(isConnected(valueAtom)).toBe(false)
    expect(element.className).toBe('aaa')

    valueAtom.set('bbb')
    await wrap(sleep())

    expect(isConnected(valueAtom)).toBe(false)
    expect(element.className).toBe('aaa')
  }))

test('preserves atom connection when moved within DOM', () =>
  context.start(async () => {
    const valueAtom = atom('aaa')
    const element = <div class={valueAtom}></div>

    mount(parent(), element)
    await wrap(sleep())
    parent().parentElement!.append(element)
    await wrap(sleep())
    valueAtom.set('bbb')
    await wrap(sleep())

    expect(isConnected(valueAtom)).toBe(true)
    expect(element.className).toBe('bbb')
  }))

/**
 * @todo `second.subscribe` is called twice with the same DocumentFragment so
 *   the second time the children are deleted.
 */
test.skip('fragment as child in double atom', () =>
  context.start(async () => {
    const first = computed(() => <div>{second}</div>, 'first')
    const second = computed(() => <>test</>, 'second')
    const element = <div>{first}</div>

    mount(parent(), element)
    await wrap(sleep())

    expect(element.innerHTML).toBe(
      '<!--first--><p><!--second--><!---->test<!----><!--second--></p><!--first-->',
    )
  }))
